<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="coinfaces">

        <title>Mokee呼叫黑名单里的通配符</title>

    <link rel="stylesheet" href="../../theme/css/style.css">
    <link rel="stylesheet" href="../../theme/css/pygments.css">



</head>

<body>
    <div class="content">
<h1>Mokee呼叫黑名单里的通配符</h1>    <p>
        under
            <a href="../../tags/android/">android</a>
            <a href="../../tags/mokee/">mokee</a>
    </p>
    <p>
        in <a href="../../categories/tech/">tech</a>
    </p>
    <p>Published: 2016-06-17</p>


    <p><a class="reference external" href="http://www.mokeedev.com/">Mokee</a>的来电和短信黑名单（设置-&gt;隐私-&gt;呼叫黑名单列表）里可以使用通配符，在呼叫黑名单列表的设置页面里勾选“使用通配符”就可打开这个功能。通配符可以让用户方便地屏蔽一个号码段内的所有号码，但使用起来并不直观，我刚看到这个功能的时候，就试图用它来屏蔽一个号码段的骚扰电话，两次都没成功，直到今天翻了源代码才恍然大悟。这个功能在使用的时候，容易使用户产生两个误解，下面会试图澄清这两个误解，并用代码来支持我的观点。</p>
<div class="section" id="id2">
<h2>黑名单数据库结构</h2>
<p>为了更容易理解代码，先看以下黑名单数据库是什么样子的，打开adb root，将黑名单数据库拉下来（<a class="reference external" href="http://bbs.mfunz.com/thread-949783-1-1.html">感谢魔趣论坛的brdsz用户的分享</a>）：</p>
<pre class="literal-block">
adb pull /data/data/com.android.providers.telephony/databases/blacklist.db
</pre>
<p>输入下面的命令就可以一目了然地看到数据库的结构：</p>
<pre class="literal-block">
sqlite3 line blacklist.db '.schema blacklist'
sqlite3 blacklist.db 'select * from blacklist;'
</pre>
<p>大致如下：</p>
<table border="1" class="docutils">
<colgroup>
<col width="6%" />
<col width="22%" />
<col width="33%" />
<col width="16%" />
<col width="10%" />
<col width="14%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">_id</th>
<th class="head">number</th>
<th class="head">normalized_number</th>
<th class="head">is_regex</th>
<th class="head">phone</th>
<th class="head">message</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>7</td>
<td>123456789</td>
<td>123456789</td>
<td>0</td>
<td>1</td>
<td>1</td>
</tr>
<tr><td>8</td>
<td>123.456*</td>
<td>123_456%</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr><td>9</td>
<td>02012345678</td>
<td>+862012345678</td>
<td>0</td>
<td>1</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>_id是不重复的序号，每添加一条记录就加1，number是用户输入的，需被屏蔽的号码，normalized_number是经过程序处理，在程序内部用于各种操作的号码，is_regex指示这条记录是否是通配符，phone和message分别表示屏蔽呼叫或短信。注意这里用is_regex来命名通配符标识，那到底上表中的<tt class="docutils literal">123.456*</tt>是通配符还是正则表达式，这是第一个容易引起误解的地方。</p>
</div>
<div class="section" id="id4">
<h2>通配符还是正则表达式？</h2>
<p>尽管明确说这是通配符，但估计会写程序的用户很容易联想到正则表达式，原因是关于该选项的说明“使用<tt class="docutils literal">.</tt>作为通配符和<tt class="docutils literal">*</tt>作为重复字符”，这和正则表达式是一致的，加上给出的例子<tt class="docutils literal">123.*</tt>表示所有以123为前缀的号码，<tt class="docutils literal">.*123.*</tt>表示所有包含123的号码，也是正则表达式里的典型用法。将号码和黑名单数据库里的值进行比较的核心代码在<a class="reference external" href="https://github.com/MoKee/android_packages_services_Telephony">android_packages_providers_TelephonyProvider</a>项目“src/com/android/providers/telephony/BlacklistProvider.java”文件里的<tt class="docutils literal">query</tt>函数：</p>
<div class="highlight"><pre><span></span><span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">Cursor</span> <span class="nf">query</span><span class="p">(</span><span class="n">Uri</span> <span class="n">uri</span><span class="p">,</span> <span class="n">String</span><span class="o">[]</span> <span class="n">projection</span><span class="p">,</span> <span class="n">String</span> <span class="n">selection</span><span class="p">,</span>
        <span class="n">String</span><span class="o">[]</span> <span class="n">selectionArgs</span><span class="p">,</span> <span class="n">String</span> <span class="n">sortOrder</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">SQLiteQueryBuilder</span> <span class="n">qb</span> <span class="o">=</span> <span class="k">new</span> <span class="n">SQLiteQueryBuilder</span><span class="p">();</span>

    <span class="n">qb</span><span class="p">.</span><span class="na">setTables</span><span class="p">(</span><span class="n">BLACKLIST_TABLE</span><span class="p">);</span>

    <span class="c1">// Generate the body of the query.</span>
    <span class="kt">int</span> <span class="n">match</span> <span class="o">=</span> <span class="n">sURIMatcher</span><span class="p">.</span><span class="na">match</span><span class="p">(</span><span class="n">uri</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">DEBUG</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Log</span><span class="p">.</span><span class="na">v</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="s">&quot;Query uri=&quot;</span> <span class="o">+</span> <span class="n">uri</span> <span class="o">+</span> <span class="s">&quot;, match=&quot;</span> <span class="o">+</span> <span class="n">match</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">switch</span> <span class="p">(</span><span class="n">match</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">BL_ALL</span><span class="p">:</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="n">BL_ID</span><span class="p">:</span>
            <span class="n">qb</span><span class="p">.</span><span class="na">appendWhere</span><span class="p">(</span><span class="n">Blacklist</span><span class="p">.</span><span class="na">_ID</span> <span class="o">+</span> <span class="s">&quot; = &quot;</span> <span class="o">+</span> <span class="n">uri</span><span class="p">.</span><span class="na">getLastPathSegment</span><span class="p">());</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="n">BL_NUMBER</span><span class="p">:</span> <span class="p">{</span>
            <span class="n">String</span> <span class="n">number</span> <span class="o">=</span> <span class="n">BlacklistUtils</span><span class="p">.</span><span class="na">normalizeNumber</span><span class="p">(</span><span class="n">getContext</span><span class="p">(),</span>
                    <span class="n">uri</span><span class="p">.</span><span class="na">getLastPathSegment</span><span class="p">()).</span><span class="na">first</span><span class="p">;</span>
            <span class="kt">boolean</span> <span class="n">regex</span> <span class="o">=</span> <span class="n">uri</span><span class="p">.</span><span class="na">getBooleanQueryParameter</span><span class="p">(</span><span class="n">Blacklist</span><span class="p">.</span><span class="na">REGEX_KEY</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">regex</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">qb</span><span class="p">.</span><span class="na">appendWhere</span><span class="p">(</span><span class="s">&quot;\&quot;&quot;</span> <span class="o">+</span> <span class="n">number</span> <span class="o">+</span> <span class="s">&quot;\&quot; like &quot;</span> <span class="o">+</span> <span class="n">COLUMN_NORMALIZED</span><span class="p">);</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="n">qb</span><span class="p">.</span><span class="na">appendWhere</span><span class="p">(</span><span class="n">COLUMN_NORMALIZED</span> <span class="o">+</span> <span class="s">&quot; = \&quot;&quot;</span> <span class="o">+</span> <span class="n">number</span> <span class="o">+</span> <span class="s">&quot;\&quot;&quot;</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">case</span> <span class="n">BL_PHONE</span><span class="p">:</span>
            <span class="n">qb</span><span class="p">.</span><span class="na">appendWhere</span><span class="p">(</span><span class="n">Blacklist</span><span class="p">.</span><span class="na">PHONE_MODE</span> <span class="o">+</span> <span class="s">&quot; != 0&quot;</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="n">BL_MESSAGE</span><span class="p">:</span>
            <span class="n">qb</span><span class="p">.</span><span class="na">appendWhere</span><span class="p">(</span><span class="n">Blacklist</span><span class="p">.</span><span class="na">MESSAGE_MODE</span> <span class="o">+</span> <span class="s">&quot; != 0&quot;</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">default</span><span class="p">:</span>
            <span class="n">Log</span><span class="p">.</span><span class="na">e</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="s">&quot;query: invalid request: &quot;</span> <span class="o">+</span> <span class="n">uri</span><span class="p">);</span>
            <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">TextUtils</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">(</span><span class="n">sortOrder</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">sortOrder</span> <span class="o">=</span> <span class="n">Blacklist</span><span class="p">.</span><span class="na">DEFAULT_SORT_ORDER</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">SQLiteDatabase</span> <span class="n">db</span> <span class="o">=</span> <span class="n">mOpenHelper</span><span class="p">.</span><span class="na">getReadableDatabase</span><span class="p">();</span>
    <span class="n">Cursor</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">qb</span><span class="p">.</span><span class="na">query</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">projection</span><span class="p">,</span> <span class="n">selection</span><span class="p">,</span> <span class="n">selectionArgs</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="n">sortOrder</span><span class="p">);</span>

    <span class="n">ret</span><span class="p">.</span><span class="na">setNotificationUri</span><span class="p">(</span><span class="n">getContext</span><span class="p">().</span><span class="na">getContentResolver</span><span class="p">(),</span> <span class="n">uri</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">ret</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>在<tt class="docutils literal">switch(match)</tt>的<tt class="docutils literal">case BL_NUMBER:</tt>分支里可以看到，如果这条记录设置了regex标识，就用SQL的<a class="reference external" href="https://sqlite.org/lang_expr.html#like">LIKE运算符</a>来匹配，否则就直接用<tt class="docutils literal">=</tt>运算符来匹配。SQL <a class="reference external" href="https://sqlite.org/lang_expr.html#like">LIKE</a>用<tt class="docutils literal">_</tt>匹配任意单个字符（对应设置界面里输入的<tt class="docutils literal">.</tt>），用<tt class="docutils literal">%</tt>匹配任意长度（包括0）的任意字符串（对应设置界面里输入的<tt class="docutils literal">*</tt>）。严格地说这样的表达式应称为通配符，在变量名里使用regex不合适，并且用户界面里给出的两个例子是错误的，因为它们都不能匹配<tt class="docutils literal">123</tt>。注意这里匹配的双方都叫做正规化的号码（Normalized Number），这是另一个容易引起误解的地方。</p>
<p>但这个错误并非Mokee项目引入的，<a class="reference external" href="https://github.com/CyanogenMod/android_packages_providers_TelephonyProvider/blob/cm-13.0/src/com/android/providers/telephony/BlacklistProvider.java">上游代码里</a>就已经这样写了。</p>
</div>
<div class="section" id="id8">
<h2>正规化号码</h2>
<p>号码的正规化处理在<a class="reference external" href="https://github.com/MoKee/android_frameworks_opt_telephony">android_frameworks_opt_telephony</a>项目“src/java/com/android/internal/telephony/util/BlacklistUtils.java”文件的<tt class="docutils literal">normalizeNumber</tt>函数：</p>
<div class="highlight"><pre><span></span><span class="cm">/**</span>
<span class="cm"> * Normalizes the passed in number and tries to format it according to E164.</span>
<span class="cm"> * Returns a pair of</span>
<span class="cm"> * - normalized number</span>
<span class="cm"> * - boolean indicating whether the number is a E164 number or not</span>
<span class="cm"> */</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="n">Pair</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span> <span class="n">Boolean</span><span class="o">&gt;</span> <span class="nf">normalizeNumber</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">,</span> <span class="n">String</span> <span class="n">number</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">len</span> <span class="o">=</span> <span class="n">number</span><span class="p">.</span><span class="na">length</span><span class="p">();</span>
    <span class="n">StringBuilder</span> <span class="n">ret</span> <span class="o">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="p">(</span><span class="n">len</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="n">number</span><span class="p">.</span><span class="na">charAt</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
        <span class="c1">// Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)</span>
        <span class="kt">int</span> <span class="n">digit</span> <span class="o">=</span> <span class="n">Character</span><span class="p">.</span><span class="na">digit</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">digit</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">ret</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">digit</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">((</span><span class="n">c</span> <span class="o">&gt;=</span> <span class="sc">&#39;a&#39;</span> <span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">&lt;=</span> <span class="sc">&#39;z&#39;</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="n">c</span> <span class="o">&gt;=</span> <span class="sc">&#39;A&#39;</span> <span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">&lt;=</span> <span class="sc">&#39;Z&#39;</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">String</span> <span class="n">actualNumber</span> <span class="o">=</span> <span class="n">PhoneNumberUtils</span><span class="p">.</span><span class="na">convertKeypadLettersToDigits</span><span class="p">(</span><span class="n">number</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">normalizeNumber</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">actualNumber</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">==</span> <span class="sc">&#39;+&#39;</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">ret</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">c</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">&#39;*&#39;</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// replace regex match-multiple character by SQL equivalent</span>
            <span class="n">ret</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="sc">&#39;%&#39;</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">&#39;.&#39;</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// replace regex-match-single character by SQL equivalent</span>
            <span class="n">ret</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="sc">&#39;_&#39;</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">String</span> <span class="n">normalizedNumber</span> <span class="o">=</span> <span class="n">ret</span><span class="p">.</span><span class="na">toString</span><span class="p">();</span>
    <span class="n">String</span> <span class="n">e164Number</span> <span class="o">=</span> <span class="n">toE164Number</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">normalizedNumber</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">Pair</span><span class="p">.</span><span class="na">create</span><span class="p">(</span><span class="n">e164Number</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">e164Number</span> <span class="p">:</span> <span class="n">normalizedNumber</span><span class="p">,</span> <span class="n">e164Number</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="n">String</span> <span class="nf">toE164Number</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">,</span> <span class="n">String</span> <span class="n">src</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Try to retrieve the current ISO Country code</span>
    <span class="n">TelephonyManager</span> <span class="n">tm</span> <span class="o">=</span> <span class="p">(</span><span class="n">TelephonyManager</span><span class="p">)</span>
            <span class="n">context</span><span class="p">.</span><span class="na">getSystemService</span><span class="p">(</span><span class="n">Context</span><span class="p">.</span><span class="na">TELEPHONY_SERVICE</span><span class="p">);</span>
    <span class="n">String</span> <span class="n">countryCode</span> <span class="o">=</span> <span class="n">tm</span><span class="p">.</span><span class="na">getSimCountryIso</span><span class="p">();</span>
    <span class="n">Locale</span> <span class="n">numberLocale</span> <span class="o">=</span> <span class="n">TextUtils</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">(</span><span class="n">countryCode</span><span class="p">)</span>
            <span class="o">?</span> <span class="n">context</span><span class="p">.</span><span class="na">getResources</span><span class="p">().</span><span class="na">getConfiguration</span><span class="p">().</span><span class="na">locale</span>
            <span class="p">:</span> <span class="k">new</span> <span class="n">Locale</span><span class="p">(</span><span class="s">&quot;&quot;</span><span class="p">,</span> <span class="n">countryCode</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">PhoneNumberUtils</span><span class="p">.</span><span class="na">formatNumberToE164</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">numberLocale</span><span class="p">.</span><span class="na">getCountry</span><span class="p">());</span>
<span class="p">}</span>
</pre></div>
<p>这里可以清楚地看到程序将用户输入的<tt class="docutils literal">*</tt>和<tt class="docutils literal">.</tt>分别替换成了<tt class="docutils literal">%</tt>和<tt class="docutils literal">_</tt>即<a class="reference external" href="https://sqlite.org/lang_expr.html#like">LIKE</a>的通配符模式，最后调用<tt class="docutils literal">toE164Number</tt>函数，这个函数的大致效果是把区号最前面的0换成了中国的<tt class="docutils literal">+86</tt>代码，例如广州号码<tt class="docutils literal">02012345678</tt>被转化为<tt class="docutils literal">+862012345678</tt>，但<tt class="docutils literal">toE164Number</tt>对通配符无效，<tt class="docutils literal">02012345*</tt>最终会被转化为<tt class="docutils literal">02012345%</tt>，这个正则表达式并不能匹配<tt class="docutils literal">+862012345678</tt>，这也是我两次都没有能屏蔽成功的原因。如果用户想屏蔽<tt class="docutils literal">02012345</tt>开头的号码段，应该输入<tt class="docutils literal">+862012345*</tt>。</p>
</div>
<div class="section" id="id10">
<h2>小结</h2>
<p>使用通配符时应把前缀填写完全，我用到的号码都是将区号前面的0去掉换成+86，或许在没有明确的文档的情况下，最保险的方法是添加一两个被屏蔽号码的例子，把数据库文件拉下来看看究竟转化成E164号码后是什么样。用符号“.”匹配任意单个字符，用符号“*”匹配任意长度（包括0）的任意字符，而不要像例子里写的那样用“.*”。</p>
<p>（完）</p>
</div>

    </div>
<nav class="nav sidebar">
    <div class="siteinfo">
        <div class="sitename"><a href="../..">coinfaces</a></div>
        <div class="tagline">Another Side of Life</div>
    </div>

        <a href="../../categories/">categories</a>
        <a href="../..">&larr; Home</a>
</nav><footer class="footer">
    <span>
        &copy; jpli &middot;
        under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>
    </span>
    |
    <span>
        Powered by:
        <a href="http://blog.getpelican.com/">Pelican</a>
    </span>
</footer></body>
</html>